﻿// =====================================================================
//
//  This file is part of the Microsoft Dynamics CRM SDK code samples.
//
//  Copyright (C) Microsoft Corporation.  All rights reserved.
//
//  This source code is intended only as a supplement to Microsoft
//  Development Tools and/or on-line documentation.  See these other
//  materials for detailed information regarding Microsoft code samples.
//
//  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//
// =====================================================================

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Windows;
using SL.IdentityModel.Claims;
using SL.IdentityModel.Protocols.WSTrust;

namespace SL.IdentityModel.Services
{
	/// <summary>
	/// Class manages the ClaimsIdentitySession
	/// can set session cookie given an issued token, sign out of session, get session information from server to populate the 
	/// user idenity information.
	/// </summary>
	public class ClaimsIdentitySessionManager : INotifyPropertyChanged
#if SILVERLIGHT
        , IApplicationService 
#else
    // In WinRT, user must subscribe to Application events to start/stop manager.
#endif
    {
		public delegate UsernameCredentials UsernameRequiredEventHandler(object sender, string endpoint);

		private AuthenticationServiceClient _authenticationServiceClient;
		private static ClaimsIdentitySessionManager _current = null;
		private ISecurityTokenService _identityProvider = null;
		private static object _lock = new object();
		private TokenCache _tokenCache = new TokenCache();
		private IClaimsPrincipal _user = null;

		public event EventHandler<ClaimsIdentityEventArgs> GetClaimsIdentityComplete;
		public event EventHandler<SignOutEventArgs> SignOutComplete;
		public event EventHandler<InvokeAsyncCompleteEventArgs> InvokeAsyncComplete;
		public event EventHandler<SignInEventArgs> SignInComplete;
		public event PropertyChangedEventHandler PropertyChanged;
		public event UsernameRequiredEventHandler UsernameRequired;

		public Uri AuthenticationServiceEndPoint { get; set; }

		public string ApplicationIdentifier { get; set; }

		/// <summary>
		/// gets current session manager for current session information and management 
		/// </summary>
		public static ClaimsIdentitySessionManager Current
		{
			get
			{
				if (_current == null)
				{
					lock (_lock)
					{
						if (_current == null)
						{
							_current = new ClaimsIdentitySessionManager();
						}
					}
				}
				return _current;
			}
		}

		/// <summary>
		/// Configures identity provider security token service
		/// </summary>
		public ISecurityTokenService IdentityProvider
		{
			get
			{
				return _identityProvider;
			}
			set
			{
				_identityProvider = value;
			}
		}

		/// <summary>
		/// Current user information
		/// </summary>
		public IClaimsPrincipal User
		{
			get { return _user; }

			private set
			{
				_user = value;
				NotifyPropertyChanged("User");
			}
		}

		/// <summary>
		/// Creates the Current Claims session manager
		/// This is public to allow class to be created from app.xaml
		/// sets default path to AuthenticationServie.svc
		/// </summary>
		public ClaimsIdentitySessionManager()
		{
			if (null == _current)
			{
				//               this.AuthenticationServiceEndPoint = new Uri( Application.Current.Host.Source, "../AuthenticationService.svc" );
				_current = this;
			}
		}

		public void InvokeAsync(Action asyncOperation, IClientChannel innerChannel)
		{
			InvokeAsync(asyncOperation, innerChannel, WSTrust13Constants.KeyTypes.Symmetric);
		}

		/// <summary>
		/// Calls given an Action containing a WCF async proxy call, addeds an issuedToken to message header.
		/// if no token for the service is in the cache, a new one will be aquired from the configures ws-trust identityprovider.
		/// This may result in credentials having to be specified, this can be done using the UsernameRequired delegate
		/// </summary>
		/// <example>
		/// .InvokeAsync( ()=>{ client.doCallAsync(callParam);}, client.innerChannel );
		/// </example>
		/// <param name="asyncOperation">Action calling proxy fuction.</param>
		/// <param name="innerChannel">client proxy innerChannel</param>
		public void InvokeAsync(Action asyncOperation, IClientChannel innerChannel, string keyType)
		{
			IRequestCredentials credentials = null;
			WSTrustBinding binding = null;

			if (null == asyncOperation)
			{
				throw new ArgumentNullException("asyncOperation");
			}

			if (null == innerChannel)
			{
				throw new ArgumentNullException("innerChannel");
			}

			if (!(this.IdentityProvider is WSTrustSecurityTokenService))
			{
				throw new ArgumentException("IdentityProvider is not a WSTrustSecurityTokenService");
			}

			string serviceAppliesTo = innerChannel.Via.ToString();

			if (_tokenCache.HasTokenInCache(serviceAppliesTo))
			{
				DoServiceCall(serviceAppliesTo, asyncOperation, innerChannel);
			}
			else
			{
				WSTrustSecurityTokenService sts = this.IdentityProvider as WSTrustSecurityTokenService;

				switch (sts.CredentialType)
				{
					case WSTrustBindingCredentialType.Username:

						if (null == UsernameRequired)
						{
							throw new ArgumentNullException("UsernameRequired");
						}
						credentials = UsernameRequired(this, sts.Endpoint);
						binding = new WSTrustBindingUsernameMixed();
						break;
					case WSTrustBindingCredentialType.DefaultCredential:
						credentials = new WindowsCredentials();
						binding = new WSTrustBindingWindows();
						break;
					default:
						throw new ArgumentException("WSTrustBindingType not configurable");
				}

				EndpointAddress stsEndpoint = new EndpointAddress(sts.Endpoint);
				WSTrustClient trustClient = new WSTrustClient(binding, stsEndpoint, credentials);
				trustClient.IssueCompleted += new EventHandler<IssueCompletedEventArgs>(trustClient_MakeServiceCall);
				RequestSecurityToken rst = new RequestSecurityToken(keyType);
				rst.AppliesTo = new EndpointAddress(serviceAppliesTo);

				trustClient.IssueAsync(rst, new ServiceCallState(asyncOperation, innerChannel, serviceAppliesTo));
			}
		}

		/// <summary>
		/// Signs into current session with username password, to authenticate to a ws-trust sts.
		/// this then sets a session cookie that can be used to call cookie protected services
		/// </summary>
		/// <param name="username"></param>
		/// <param name="password"></param>
		public void SignInUsernameAsync(string username, string password)
		{
			if (!(this.IdentityProvider is WSTrustSecurityTokenService) ||
				(WSTrustBindingCredentialType.Username != (this.IdentityProvider as WSTrustSecurityTokenService).CredentialType))
			{
				throw new Exception("WSTrustSecurityTokenService with username binding is not configured");
			}

			GetToken((this.IdentityProvider as WSTrustSecurityTokenService).Endpoint,
				this.ApplicationIdentifier,
				new WSTrustBindingUsernameMixed(),
				new UsernameCredentials(username, password));
		}

		/// <summary>
		/// Signs into current session with current login session
		/// </summary>
		/// <param name="username"></param>
		/// <param name="password"></param>
		public void SignInDefaultCredentials()
		{
			if (!(this.IdentityProvider is WSTrustSecurityTokenService) ||
			   (WSTrustBindingCredentialType.DefaultCredential != (this.IdentityProvider as WSTrustSecurityTokenService).CredentialType))
			{
				throw new Exception("WSTrustSecurityTokenService with DefaultCredential is not configured");
			}

			GetToken((this.IdentityProvider as WSTrustSecurityTokenService).Endpoint,
				this.ApplicationIdentifier,
				new WSTrustBindingWindows(),
				new WindowsCredentials());
		}

		/// <summary>
		/// Signs out user by clearing session cookie
		/// </summary>
		public void SignOutAsync()
		{
			_authenticationServiceClient.SignOutCompleted += new EventHandler<AsyncCompletedEventArgs>(AuthenticationServiceClient_SignOutCompleted);
			_authenticationServiceClient.SignOutAsync();
		}

		/// <summary>
		/// Gets users claims information from current session, and populates User field
		/// </summary>
		public void GetClaimsIdentityAsync()
		{
			_authenticationServiceClient.SignInCompleted += new EventHandler<SignInCompletedEventArgs>(AuthenticationServiceClient_GetClaimsIdentityComplete);
			_authenticationServiceClient.SignInAsync();
		}

		/// <summary>
		/// Given a issued token, sets session cookie by calling AuthenticationService.svc
		/// </summary>
		/// <param name="issuedToken"></param>
		public void SetSessionCookieAsync(string issuedToken)
		{
			_authenticationServiceClient.SignInWithIssuedTokenCompleted += new EventHandler<SignInWithIssuedTokenCompletedEventArgs>(AuthenticationServiceClient_SignInWithIssuedTokenCompleted);
			_authenticationServiceClient.SignInWithIssuedTokenAsync(issuedToken);
		}

		/// <summary>
		/// called onse session cookie is set, using issued token
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		void AuthenticationServiceClient_SignInWithIssuedTokenCompleted(object sender, SignInWithIssuedTokenCompletedEventArgs e)
		{
			if (null == e.Error)
			{
				this.User = CreateIClaimsPrincipal(e.Result);

				if (null != SignInComplete)
				{
					SignInComplete(sender, new SignInEventArgs(User, e.Error));
				}
			}
			else
			{
				throw e.Error;
			}
		}

		/// <summary>
		/// calls a WCF service with an issuedToken binding, placing the issuedToken in the message header
		/// </summary>
		/// <param name="serviceAppliesTo"></param>
		/// <param name="asyncOperation"></param>
		/// <param name="contextChannel"></param>
		private void DoServiceCall(string serviceAppliesTo, Action asyncOperation, IContextChannel contextChannel)
		{
			using (OperationContextScope scope = new OperationContextScope(contextChannel))
			{
				IssuedTokenHeader header = new IssuedTokenHeader(_tokenCache.GetTokenFromCache(serviceAppliesTo));
				OperationContext.Current.OutgoingMessageHeaders.Add(header);
				asyncOperation();
			}
		}

		/// <summary>
		/// Calls ws-trust security token service to request a token
		/// </summary>
		/// <param name="stsEndpoint">security token service endpoint url</param>
		/// <param name="appliesTo">appliesTo identifing target which the token is intended for</param>
		/// <param name="binding">WCF binding to use for the call</param>
		/// <param name="credentials">credentials to use to authenticate to the security token service</param>
		private void GetToken(string stsEndpoint, string appliesTo, Binding binding, IRequestCredentials credentials)
		{
			if (null == stsEndpoint)
			{
				throw new ArgumentNullException("stsEndpoint");
			}

			if (null == appliesTo)
			{
				throw new ArgumentNullException("appliesTo");
			}

			if (null == binding)
			{
				throw new ArgumentNullException("binding");
			}

			if (null == credentials)
			{
				throw new ArgumentNullException("credentials");
			}

			WSTrustClient trustClient = new WSTrustClient(binding, new EndpointAddress(stsEndpoint), credentials);

			trustClient.IssueCompleted += new EventHandler<IssueCompletedEventArgs>(trustClient_IssueCompleted);

			RequestSecurityToken rst = new RequestSecurityToken(WSTrust13Constants.KeyTypes.Bearer);
			rst.AppliesTo = new EndpointAddress(appliesTo);

			trustClient.IssueAsync(rst, new ServiceCallState(null, null, appliesTo));
		}

		/// <summary>
		/// Called on return of GetClaimsIdentity async call
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void AuthenticationServiceClient_GetClaimsIdentityComplete(object sender, SignInCompletedEventArgs e)
		{

			if (null != e.Error)
			{
				this.User = null;
			}
			else
			{
				this.User = CreateIClaimsPrincipal(e.Result);
			}

			if (null != this.GetClaimsIdentityComplete)
			{
				this.GetClaimsIdentityComplete(sender, new ClaimsIdentityEventArgs(this.User, e.Error));
			}
		}

		/// <summary>
		/// given claims collection from AthenticationService.svc, creates a ClaimsPrincipal
		/// </summary>
		/// <param name="claims"></param>
		/// <returns></returns>
		private IClaimsPrincipal CreateIClaimsPrincipal(ObservableCollection<AuthenticationServiceIdentityClaim> claims)
		{
			IClaimsIdentity identity = new ClaimsIdentity();

			foreach (AuthenticationServiceIdentityClaim c in claims)
			{
				identity.Claims.Add(new Claim(c.Issuer, c.OriginalIssuer, c.Type, c.Value, c.ValueType));
			}

			return new ClaimsPrincipal(identity);
		}

		/// <summary>
		/// returns after async call to SignOut user is completed
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void AuthenticationServiceClient_SignOutCompleted(object sender, AsyncCompletedEventArgs e)
		{
			if (null == e.Error)
			{
				this.User = new ClaimsPrincipal(new ClaimsIdentity());
			}

			if (null != this.SignOutComplete)
			{
				this.SignOutComplete(sender, new SignOutEventArgs(e.Error));
			}
		}

		/// <summary>
		/// /// returns after async call to Issue is completed
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void trustClient_IssueCompleted(object sender, IssueCompletedEventArgs e)
		{
			if (null == e.Error)
			{
				ClaimsIdentitySessionManager.Current.SetSessionCookieAsync(e.Result.RequestedSecurityToken.RawToken);
			}
			else if (null != SignInComplete)
			{
				SignInComplete(sender, new SignInEventArgs(null, e.Error));
			}
		}

		private void NotifyPropertyChanged(String info)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(info));
			}
		}

		/// <summary>
		/// called after a ws-trust call is made to get a issued token for the service call
		/// the service is then called using the token to authenticate
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void trustClient_MakeServiceCall(object sender, IssueCompletedEventArgs e)
		{
			if (null == e.Error)
			{
				RequestSecurityTokenResponse rstr = e.Result;
				string appliesTo;

				if (null == rstr.AppliesTo || null == rstr.AppliesTo.Uri)
				{
					throw new ArgumentException("No appliesTo in RequestSecurityTokenResponse");
				}

				appliesTo = rstr.AppliesTo.Uri.AbsoluteUri;
				_tokenCache.AddTokenToCache(rstr.AppliesTo.Uri.AbsoluteUri, rstr);
				ServiceCallState state = e.UserState as ServiceCallState;
				DoServiceCall(appliesTo, state.Action, state.Channel);
			}
			else
			{
				if (null != InvokeAsyncComplete)
				{
					InvokeAsyncComplete(sender, new InvokeAsyncCompleteEventArgs(e.Error));
				}
			}
		}

		#region IApplicationService

#if SILVERLIGHT
		public void StartService(ApplicationServiceContext context)
		{
			Application.Current.Resources.Add("ClaimsIdentitySessionManager", Current);

			_authenticationServiceClient = new AuthenticationServiceClient(
					new CustomBinding(
						new BinaryMessageEncodingBindingElement(),
						new HttpsTransportBindingElement()
						), new EndpointAddress(this.AuthenticationServiceEndPoint));

			this.User = new ClaimsPrincipal(new ClaimsIdentity());

			if (Current.IdentityProvider is WSFederationSecurityTokenService)
			{
				this.GetClaimsIdentityAsync();
			}
		}

		public void StopService()
		{ }
#endif

        #endregion
    }
}
